Android自定义View

您所在的位置:网站首页 android 自定义view onlayout Android自定义View

Android自定义View

2023-06-20 10:13| 来源: 网络整理| 查看: 265

1.什么是自定义View? 1. 定义

在Android系统中,看到的应用界面都是View,界面也就是由一个个View组成的,AndroidSdk中为开发者提供了形形色色的View,比如:显示文字的TextView,显示图片的ImageView,显示列表数据的ListView等等.但是在开发想实现一个折线统计图,这时候系统将不会在满足需求,需要开发者去通过自定义view来实现.

2. 如何实现

自定义 View 就是通过继承 View 或者 View 的子类,并在新的类里面实现相应的处理逻辑(重写相应的方法),以达到自己想要的效果。

3. View视图框架结构

微信截图_20210901171305.png

通过流程图可以看出View是所有View的父类,实现一个View都是要继承View,并且可以看出View分为两大类View和ViewGroup,带着这个问题我们去看View和ViewGroup有什么不同之处.

2. 为什么使用自定义View

在开发中,开发者常常会因为下面四个主要原因去自定义 View:

让界面有特定的显示风格、效果; 让控件具有特殊的交互方式; 优化布局; 封装; 2.1让界面有特定的显示风格、效果

在开发中,Android SDK提供了很多控件,但有时,这些控件并不能满足业务需求。例如,想要用一个折线图来展示一组数据,这时如果用系统提供的 View 就不能实现了,只能通过自定义 View 来实现。

2.2 让控件具有特殊的交互方式

Android SDK提供的控件都有属于它们自己的特定的交互方式,但有时,控件的默认交互方式并不能满足业务的需求。例如,开发者想要缩放 ImageView 中的图片内容,这时如果用系统提供的 ImageView 就不能实现了,只能通过自定义 ImageView 来实现。

2.3 优化布局

有时,有些布局如果用系统提供的控件实现起来相当复杂,需要各种嵌套,虽然最终也能实现了想要的效果,但性能极差,此时就可以通过自定义 View 来减少嵌套层级、优化布局。

2.4 封装

有些控件可能在多个地方使用,如大多数 App 里面的底部 Tab,像这样的经常被用到的控件就可以通过自定义 View 将它们封装起来,以便在多个地方使用。

3.如何自定义View?

在了解如何自定义View前,我们先要知道自定义View包含哪些内容? 自定义View包含三部分

layout(view的布局) draw(view的绘制) 触摸反馈(view的点击事件) 在布局阶段我们要知道View的尺寸和位置,在绘制阶段我吗要知道view的内容,触摸反馈我们要得到view的点击事件响应.

其中布局阶段包括测量(measure)和布局(layout)两个过程,另外view的绘布局阶段是为view的绘制和触摸反馈做支持的,当确定了view的位置我们才能去绘制view设置view的触摸反馈

在自定义 View 和自定义 ViewGroup 中,布局和绘制流程虽然整体上都是一样的,但在细节方面,自定义 View 和自定义 ViewGroup 还是不一样的,所以,接下来分两类进行讨论:

自定义 View 布局、绘制流程 自定义 ViewGroup 布局、绘制流程 3.1自定义 View 布局、绘制流程 3.2自定义View最基本的流程图

微信截图_20211027102112.png 从View继承一般需要忙活的方法是onDraw这里

3.3构造函数 (获取自定义参数)

构造函数中我们主要会做一些初始化操作,以及获取自己的自定义属性参数(如果使用自定义属性的话), 如果有使用自定义属性的话,我们可以通过AttributeSet对象attrs获取他们的值

public LineChartView(Context context) { super(context); } public LineChartView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public LineChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public LineChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } 复制代码

不管是继承ViewGroup还是View都有四个构造重载方式可供选择,其实四个参数的是API21之后添加的。

有三个参数的构造函数中第三个参数是默认的Style,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style,且只有在明确调用的时候才会生效

我们在写自定义View的时候需要关心的通常是有一个和两个参数的构造方法

自定义 View 测量阶段

在 View 的测量阶段会执行两个方法(在测量阶段,View 的父 View 会通过调用 View 的 measure() 方法将父 View 对 View 尺寸要求传进来。紧接着 View 的 measure() 方法会做一些前置和优化工作,然后调用 View 的 onMeasure() 方法,并通过 onMeasure() 方法将父 View 对 View 的尺寸要求传入。在自定义 View 中,只有需要修改 View 的尺寸的时候才需要重写 onMeasure() 方法。在 onMeasure() 方法中根据业务需求进行相应的逻辑处理,并在最后通过调用 setMeasuredDimension() 方法告知父 View 自己的期望尺寸):

measure() onMeasure()

measure() : 调度方法,主要做一些前置和优化工作,并最终会调用 onMeasure() 方法执行实际的测量工作;

onMeasure() : 实际执行测量任务的方法,主要用与测量 View 尺寸和位置。在自定义 View 的 onMeasure() 方法中,View 根据自己的特性和父 View 对自己的尺寸要求算出自己的期望尺寸,并通过 setMeasuredDimension() 方法告知父 View 自己的期望尺寸。

onMeasure() 计算 View 期望尺寸方法如下:

参考父 View 的对 View 的尺寸要求和实际业务需求计算出 View 的期望尺寸:

解析 widthMeasureSpec;

解析 heightMeasureSpec;

将「根据实际业务需求计算出 View 的尺寸」根据「父 View 的对 View 的尺寸要求」进行相应的>修正得出 View 的期望尺寸(通过调用 resolveSize() 方法);

通过 setMeasuredDimension() 保存 View 的期望尺寸(实际上是通过 setMeasuredDimension() 告知父 View 自己的期望尺寸); onMeasure (测量View大小)

measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法。

这里面涉及到一个类MeasureSpec,MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。

SpecMode有三类,每一类都表示特殊的含义 UNSPECIFIED

父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部。

EXACTLY

父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所以定的值。她对应LayoutParams中的match_parent和具体的数值

AT_MOST

父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值。她对应于LayoutParams中的wrap_content.

onSizeChanged (确定View的大小)

这个函数在视图大小发生改变时调用。 一般情况下onMeasure中就可以把View的大小确定下来了,但是因为View的大小不仅由View本身控制,而且受父控件的影响,所以我们在确定View大小的时候最好使用系统提供的onSizeChanged回调函数。

自定义 View 布局阶段

layout() : 保存 View 的实际尺寸。调用 setFrame() 方法保存 View 的实际尺寸,调用 onSizeChanged() 通知开发者 View 的尺寸更改了,并最终会调用 onLayout() 方法让子 View 布局(如果有子 View 的话。因为自定义 View 中没有子 View,所以自定义 View 的 onLayout() 方法是一个空实现);

onLayout() : 空实现,什么也不做,因为它没有子 View。如果是 ViewGroup 的话,在 onLayout() 方法中需要调用子 View 的 layout() 方法,将子 View 的实际尺寸传给它们,让子 View 保存自己的实际尺寸。因此,在自定义 View 中,不需重写此方法,在自定义 ViewGroup 中,需重写此方法。

自定义 View 绘制阶段

在 View 的绘制阶段会执行一个方法——draw(),draw() 是绘制阶段的总调度方法,在其中会调用绘制背景的方法 drawBackground()、绘制主体的方法 onDraw()、绘制子 View 的方法 dispatchDraw() 和 绘制前景的方法 onDrawForeground():

draw()

draw() : 绘制阶段的总调度方法,在其中会调用绘制背景的方法 drawBackground()、绘制主体的方法 onDraw()、绘制子 View 的方法 dispatchDraw() 和 绘制前景的方法 onDrawForeground();

drawBackground() : 绘制背景的方法,不能重写,只能通过 xml 布局文件或者 setBackground() 来设置或修改背景;

onDraw() : 绘制 View 主体内容的方法,通常情况下,在自定义 View 的时候,只用实现该方法即可;

dispatchDraw() : 绘制子 View 的方法。同 onLayout() 方法一样,在自定义 View 中它是空实现,什么也不做。但在自定义 ViewGroup 中,它会调用 ViewGroup.drawChild() 方法,在 ViewGroup.drawChild() 方法中又会调用每一个子 View 的 View.draw() 让子 View 进行自我绘制;

onDrawForeground() : 绘制 View 前景的方法,也就是说,想要在主体内容之上绘制东西的时候就可以在该方法中实现。

注意: Android 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。

自定义 ViewGroup 布局、绘制流程 自定义 ViewGroup 测量阶段

同自定义 View 一样,在自定义 ViewGroup 的测量阶段会执行两个方法:

measure() onMeasure()

measure() : 调度方法,主要做一些前置和优化工作,并最终会调用 onMeasure() 方法执行实际的测量工作;

onMeasure() : 实际执行测量任务的方法,与自定义 View 不同,在自定义 ViewGroup 的 onMeasure() 方法中,ViewGroup 会递归调用子 View 的 measure() 方法,并通过 measure() 将 ViewGroup 对子 View 的尺寸要求(ViewGroup 会根据开发者对子 View 的尺寸要求、自己的父 View(ViewGroup 的父 View) 对自己的尺寸要求和自己的可用空间计算出自己对子 View 的尺寸要求)传入,对子 View 进行测量,并把测量结果临时保存,以便在布局阶段使用。测量出子 View 的实际尺寸之后,ViewGroup 会根据子 View 的实际尺寸计算出自己的期望尺寸,并通过 setMeasuredDimension() 方法告知父 View(ViewGroup 的父 View) 自己的期望尺寸。

具体流程如下:

运行前,开发者在 xml 中写入对 ViewGroup 和 ViewGroup 子 View 的尺寸要求 layout_xxx; ViewGroup 在自己的 onMeasure() 方法中,根据开发者在 xml 中写的对 ViewGroup 子 View 的尺寸要求、自己的父 View(ViewGroup 的父 View) 对自己的尺寸要求和自己的可用空间计算出自己对子 View 的尺寸要求,并调用每个子 View 的 measure() 将 ViewGroup 对子 View 的尺寸要求传入,测量子 View 尺寸; ViewGroup 在子 View 计算出期望尺寸之后(在 ViewGroup 的 onMeasure() 方法中,ViewGroup 递归调用每个子 View 的 measure() 方法,子 View 在自己的 onMeasure() 方法中会通过调用 setMeasuredDimension() 方法告知父 View(ViewGroup) 自己的期望尺寸),得出子 View 的实际尺寸和位置,并暂时保存计算结果,以便布局阶段使用; ViewGroup 根据子 View 的尺寸和位置计算自己的期望尺寸,并通过 setMeasuredDimension() 方法告知父 View 自己的期望尺寸。如果想要做的更好,可以在「 ViewGroup 根据子 View 的尺寸和位置计算出自己的期望尺寸」之后,再结合 ViewGroup 的父 View 对 ViewGroup 的尺寸要求进行修正(通过 resolveSize() 方法),这样得出的 ViewGroup 的期望尺寸更符合 ViewGroup 的父 View 对 ViewGroup 的尺寸要求。 自定义 ViewGroup 布局阶段

同自定义 View 一样,在自定义 ViewGroup 的布局阶段会执行两个方法:

layout() onLayout()

layout() : 保存 ViewGroup 的实际尺寸。调用 setFrame() 方法保存 ViewGroup 的实际尺寸,调用 onSizeChanged() 通知开发者 ViewGroup 的尺寸更改了,并最终会调用 onLayout() 方法让子 View 布局;

onLayout() : ViewGroup 会递归调用每个子 View 的 layout() 方法,把测量阶段计算出的子 View 的实际尺寸和位置传给子 View,让子 View 保存自己的实际尺寸和位置。

自定义 ViewGroup 绘制阶段

同自定义 View 一样,在自定义 ViewGroup 的绘制阶段会执行一个方法——draw()。draw() 是绘制阶段的总调度方法,在其中会调用绘制背景的方法 drawBackground()、绘制主体的方法 onDraw()、绘制子 View 的方法 dispatchDraw() 和 绘制前景的方法 onDrawForeground():

draw()

draw() : 绘制阶段的总调度方法,在其中会调用绘制背景的方法 drawBackground()、绘制主体的方法 onDraw()、绘制子 View 的方法 dispatchDraw() 和 绘制前景的方法 onDrawForeground();

在 ViewGroup 中,你也可以重写绘制主体的方法 onDraw()、绘制子 View 的方法 dispatchDraw() 和 绘制前景的方法 onDrawForeground()。但大多数情况下,自定义 ViewGroup 是不需要重写任何绘制方法的。因为通常情况下,ViewGroup 的角色是容器,一个透明的容器,它只是用来盛放子 View 的。

4.实战演练

折线统计图,可以拖动竖线,并且提示当前点的数值

SDGIF_Rusult_1.gif

自定义属性的声明与获取 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LineChartView); horizontal_color = typedArray.getColor(R.styleable.LineChartView_horizontal_color, Color.BLUE); horizontal_dotted_color = typedArray.getColor(R.styleable.LineChartView_horizontal_dotted_color, Color.GRAY); vertical_color = typedArray.getColor(R.styleable.LineChartView_vertical_color, Color.RED); typedArray.recycle(); 复制代码 重写onDraw

对View的绘制,首先要重写onDraw方法,在绘制过程中,我们需要用到的两个关键对象:

Paint:画笔,使用画笔对象设置画笔的设置例如颜色,线条的粗细等. Path:设置绘制点的开始和结束位置. Canvas:画布,通过drawPath将Paint和Path进行绑定后绘制完成. //Canvas:画布 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } 复制代码 举例使用 //1.1用来绘制文本的画笔 private Paint textPaint() { Paint textPaint = new Paint(); textPaint.setColor(horizontal_color); textPaint.setStrokeWidth(3); textPaint.setTextSize(30); return textPaint; } //2.1用来绘制虚线的画笔 private Paint dottedPaint() { Paint dottedPaint = new Paint(); dottedPaint.setAntiAlias(true); dottedPaint.setStyle(Paint.Style.STROKE); dottedPaint.setStrokeWidth(2); dottedPaint.setColor(horizontal_dotted_color); return dottedPaint; } //2.2虚线 用来显示折线统计图x轴的线 Paint dottedPaint = dottedPaint(); PathEffect pathEffect = new DashPathEffect(new float[]{5, 5, 5, 5}, 2); dottedPaint.setPathEffect(pathEffect); //2.3用来绘制虚线的 private void drawLineX(Canvas canvas, Paint textPaint, Paint dottedPaint) { for (int i = 0; i < lineNumY; i++) { if (lineNumY - 1 == i) {//绘制折线图x轴最底部的线 Paint paint = new Paint(); Path path = new Path(); paint.setColor(Color.BLUE); paint.setStrokeWidth(3); paint.setStyle(Paint.Style.STROKE); paint.setAntiAlias(true); path.moveTo(marginLeftRight, marginTopBottom + (i * meanHeight)); path.lineTo(getWidth() - marginLeftRight, marginTopBottom + (i * meanHeight)); canvas.drawPath(path, paint); paint.reset(); } else {//绘制折线图x轴的虚线 Path linePath = new Path(); linePath.moveTo(marginLeftRight, marginTopBottom + (i * meanHeight)); linePath.lineTo(getWidth() - marginLeftRight, marginTopBottom + (i * meanHeight)); canvas.drawPath(linePath, dottedPaint); } //x轴最右边的数值 String s = (((lineNumY - 1) - i) * meanValue) + ""; float method = textPainasureText(s); canvas.drawText(s, marginLeftRight - method - 10, marginTopBottom + (i * meanHeight) + 10, textPaint); } dottedPaint.reset(); } //用来绘制折线的画笔 private Paint brokenLinePaint() { Paint brokenLinePaint = new Paint(); brokenLinePaint.setColor(vertical_color); brokenLinePaint.setStrokeWidth(3); brokenLinePaint.setStyle(Paint.Style.STROKE); brokenLinePaint.setAntiAlias(true); return brokenLinePaint; } //绘制折线 private void drawLineChart(Canvas canvas, Paint brokenLinePaint) { coordBeans.clear(); Path pathChart = new Path(); //lintNumX 代表折线共有几个点 折线的点是根据是把x轴平均分成了多少份求出的 for (int i = 0; i < lintNumX; i++) { if (i == 0) {//折线的起点 pathChart.moveTo(marginLeftRight, marginTopBottom + ((lineNumY - 1) * meanHeight) - axisDataY[i]); } pathChart.lineTo(marginLeftRight + meanWidth * i, marginTopBottom + ((lineNumY - 1) * meanHeight) - axisDataY[i] / meanVH);//marginTopBottom + ((lineNumY - 1) * meanHeight) - math[i%6]) canvas.drawCircle(marginLeftRight + meanWidth * i, marginTopBottom + ((lineNumY - 1) * meanHeight) - axisDataY[i] / meanVH, 3, brokenLinePaint); coordBeans.add(new CoordBean(marginLeftRight + meanWidth * i, marginTopBottom + ((lineNumY - 1) * meanHeight) - axisDataY[i] / meanVH, axisDataY[i])); } canvas.drawPath(pathChart, brokenLinePaint); } 复制代码

以上是绘制折线图统计图绘制线条的流程

重写onTouchEvent(MotionEvent event)

接下来我们来看一看,当我们触摸屏幕是如何获取到折线的触摸点

首先要重写 onTouchEvent(MotionEvent event) 然后在关注 手指按下操作MotionEvent.ACTION_DOWN 手指移动操作MotionEvent.ACTION_MOVE @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { //按下 case MotionEvent.ACTION_DOWN: downX = event.getX(); downY = event.getY(); //按下后获取按下的点是否和折线图的坐标点是否重合,重合的话提示按下的点的数值,绘制y轴的线条 for (int i = 0; i < coordBeans.size(); i++) { if (Math.abs(downX - coordBeans.get(i).getCoordX()) < paddingPath / 2 && Math.abs(downY - coordBeans.get(i).getCoordY()) < paddingPath / 2) { isClick = true; isClickIndex = i; scrollX = coordBeans.get(i).getCoordX(); invalidate(); showDetails(isClickIndex); break; } } return true; //移动 case MotionEvent.ACTION_MOVE: float x = event.getX(); float y = event.getY(); //按下滑动后获取当前的点是否和折线图的坐标点是否重合,重合的话提示按下的点的数值,绘制y轴的线条 Log.i("onTouchEvent", x + "---" + y); if (x >= startX && x = startY && y


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3